<template>
    <div class="simple-upload-container">
        <div class="total-progress">
            <div class="btns">
                <el-button-group>
                    <el-button :disabled="changeDisabled">
                        <i class="el-icon-upload2 el-icon--left" size="mini"></i>选择文件
                        <input
                                v-if="!changeDisabled"
                                type="file"
                                multiple="multiple"
                                class="select-file-input"
                                :accept="accept"
                                οnclick="f.outerHTML=f.outerHTML"
                                @change="handleFileChange"
                        />
                    </el-button>
                    <el-button :disabled="uploadDisabled" @click="handleUpload"><i class="el-icon-upload el-icon--left"
                                                                                   size="mini"></i>上传
                    </el-button>
                    <el-button :disabled="pauseDisabled" @click="handlePause"><i
                            class="el-icon-video-pause el-icon--left" size="mini"></i>暂停
                    </el-button>
                    <el-button :disabled="resumeDisabled" @click="handleResume"><i
                            class="el-icon-video-play el-icon--left" size="mini"></i>恢复
                    </el-button>
                    <el-button :disabled="clearDisabled" @click="clearFiles"><i class="el-icon-video-play el-icon--left"
                                                                                size="mini"></i>清空
                    </el-button>
                </el-button-group>
                <slot name="header"></slot>
            </div>
        </div>
        <div class="file-list">
            <el-collapse v-if="uploadFiles.length" accordion>
                <el-collapse-item v-for="(item, index) in uploadFiles" :key="index">
                    <template slot="title">
                        <div class="progress-box">
                            <div class="list-item">
                                <div class="item-name">
                                    <span>{{ index + 1 }}：名称：{{ item.name }}</span>
                                </div>
                                <div class="item-size">大小：{{ item.size | transformByte }}</div>
                                <div v-if="item.hashProgress !== 100" class="item-progress">
                                    <span>{{ status === 'wait' ? '准备读取文件' : '正在读取文件' }}：</span>
                                    <el-progress :percentage="item.hashProgress"/>
                                </div>
                                <div v-else class="item-progress"><span>文件进度：</span>
                                    <el-progress :percentage="item.uploadProgress"/>
                                </div>
                                <div class="item-status">
                                    <i :class="fileStatuIcon(item.status)"></i>
                                    {{ item.status | fileStatus }}
                                </div>
                            </div>
                        </div>
                    </template>
                    <div class="item-chunk-box">
                        <el-table :data="item.chunkList" border max-height="300">
                            <el-table-column prop="index" label="#" align="center"/>
                            <el-table-column prop="hash" label="切片md5" align="center" show-overflow-tooltip/>
                            <el-table-column label="大小" align="center" width="120">
                                <template v-slot="{ row }">
                                    {{ row.size | transformByte }}
                                </template>
                            </el-table-column>
                            <el-table-column prop="uploaded" label="是否完成" align="center">
                                <template v-slot="{ row }">
                                    {{ row.uploaded ? '完成' : '进行中' }}
                                </template>
                            </el-table-column>

                            <el-table-column label="进度" align="center">
                                <template v-slot="{ row }">
                                    <el-progress v-if="!row.status || row.status === 'wait'"
                                                 :percentage="row.progress"/>
                                    <el-progress v-else :percentage="row.progress" :status="row.status"/>
                                </template>
                            </el-table-column>
                        </el-table>
                    </div>
                </el-collapse-item>
            </el-collapse>
            <slot name="tip"></slot>
        </div>
    </div>
</template>
<script>
import axios, { CancelToken } from 'axios'
import { saveObjArr, getObjArr, clearLocalStorage } from './utils/localstorage'
import Config from '@/config'

const instance = axios.create({})

let chunkSize = 10 * 1024 * 1024 // 切片大小
let fileIndex = 0 // 当前正在被遍历的文件下标

// 所有文件状态
const Status = {
  wait: 'wait',
  pause: 'pause',
  uploading: 'uploading',
  hash: 'hash',
  error: 'error',
  done: 'done',
}

// 单个文件的状态
const fileStatus = {
  wait: 'wait',
  uploading: 'uploading',
  success: 'success',
  error: 'error',
  secondPass: 'secondPass',
  pause: 'pause',
  resume: 'resume',
}
// 单个文件的状态 对应描述
const fileStatusStr = {
  wait: '待上传',
  uploading: '上传中',
  success: '成功',
  error: '失败',
  secondPass: '已秒传',
  pause: '暂停',
  resume: '恢复',
}

export default {
  name: 'SimpleUploaderContainer',
  filters: {
    transformByte(size) {
      if (!size) {
        return '0B'
      }

      const num = 1024.0 // byte

      if (size < num) {
        return `${size}B`
      }
      // eslint-disable-next-line no-restricted-properties
      if (size < Math.pow(num, 2)) {
        return `${(size / num).toFixed(2)}K`
      } // kb
      // eslint-disable-next-line no-restricted-properties
      if (size < Math.pow(num, 3)) {
        // eslint-disable-next-line no-restricted-properties
        return `${(size / Math.pow(num, 2)).toFixed(2)}M`
      } // M
      // eslint-disable-next-line no-restricted-properties
      if (size < Math.pow(num, 4)) {
        // eslint-disable-next-line no-restricted-properties
        return `${(size / Math.pow(num, 3)).toFixed(2)}G`
      } // G
      // eslint-disable-next-line no-restricted-properties
      return `${(size / Math.pow(num, 4)).toFixed(2)}T` // T
    },
    // 单个文件状态格式化
    fileStatus(status) {
      return fileStatusStr[fileStatus[status]]
    },
  },
  props: {
    headers: {
      type: Object,
      default: null,
    },
    beforeUpload: {
      type: Function,
      default: null,
    },
    accept: {
      type: String,
      default: '',
    },
    // 上传文件时携带的参数
    uploadArguments: {
      type: Object,
      default: () => ({}),
    },
    // 是否传递cookie
    withCredentials: {
      type: Boolean,
      default: false,
    },
    // 文件个数
    limit: {
      type: Number,
      default: 0,
    },
    // 文件超出个数限制时的钩子
    onExceed: {
      type: Function,
      default: () => {
      },
    },
    multiple: {
      type: Boolean,
      default: true,
    },
    // axios baseUrl
    baseUrl: {
      type: String,
      default: Config.baseURL,
    },
    // 切片大小
    chunkSize: {
      type: Number,
      default: 10 * 1024 * 1024,
    },
    // 上传并发数
    threads: {
      type: Number,
      default: 3,
    },
    // 错误重试次数
    chunkRetry: {
      type: Number,
      default: 3,
    },
  },
  data: () => ({
    uploadFiles: [],
    worker: null,
    cancels: [], // 存储要取消的请求
    tempThreads: 3,
    // 默认状态
    status: Status.wait,
  }),
  computed: {
    changeDisabled() {
      return ![Status.wait, Status.done].includes(this.status)
    },
    uploadDisabled() {
      return !this.uploadFiles.length || [Status.pause, Status.done, Status.uploading, Status.hash].includes(this.status)
    },
    pauseDisabled() {
      return [Status.hash, Status.wait, Status.done, Status.pause].includes(this.status)
    },
    resumeDisabled() {
      return ![Status.pause].includes(this.status)
    },
    clearDisabled() {
      return !this.uploadFiles.length
    },
    // eslint-disable-next-line no-unused-vars
    fileStatuIcon(status) {
      // eslint-disable-next-line func-names,no-shadow
      return function (status) {
        let className = ''
        // eslint-disable-next-line default-case
        switch (status) {
          case 'uploading':
            className = 'el-icon-loading'
            break
          case 'success':
          case 'secondPass':
            className = 'el-icon-circle-check'
            break
          case 'error':
            className = 'el-icon-circle-close'
            break
        }
        return `${className} el-icon--left`
      }
    },
  },
  destroyed() {
    this.clearFiles()
  },
  created() {
    this.setAxios()
    this.tempThreads = this.threads
  },
  methods: {
    handleFileChange(e) {
      const { files } = e.target
      console.log('handleFileChange -> file', files)
      if (!files) return

      fileIndex = 0 // 重置文件下标
      console.log('handleFileChange -> this.uploadFiles', this.uploadFiles)
      // console.log(this.limit)
      // 判断文件选择的个数
      if (this.limit && files.length > this.limit) {
        // eslint-disable-next-line no-unused-expressions
        this.onExceed && this.onExceed(files)
        return
      }

      this.status = Status.wait

      const postFiles = Array.prototype.slice.call(files)
      console.log('handleFileChange -> postFiles', postFiles)
      postFiles.forEach((item) => {
        this.handleStart(item)
      })
    },
    handleStart(rawFile) {
      // 初始化部分自定义属性
      // eslint-disable-next-line no-param-reassign
      rawFile.status = fileStatus.wait
      // eslint-disable-next-line no-param-reassign
      rawFile.chunkList = []
      // eslint-disable-next-line no-param-reassign
      rawFile.uploadProgress = 0
      // eslint-disable-next-line no-param-reassign
      rawFile.fakeUploadProgress = 0 // 假进度条，处理恢复上传后，进度条后移的问题
      // eslint-disable-next-line no-param-reassign
      rawFile.hashProgress = 0

      if (this.beforeUpload) {
        const before = this.beforeUpload(rawFile)
        if (before && before.then) {
          before.then(() => {
            this.uploadFiles.push(rawFile)
          })
        }
      }

      if (!this.beforeUpload) {
        this.uploadFiles.push(rawFile)
      }
    },
    async handleUpload() {
      console.log('handleUpload -> this.uploadFiles', this.uploadFiles)
      if (!this.uploadFiles) return
      this.status = Status.uploading
      const filesArr = this.uploadFiles
      for (let i = 0; i < filesArr.length; i++) {
        fileIndex = i
        if (['secondPass', 'success', 'error'].includes(filesArr[i].status)) {
          console.log('跳过已上传成功或已秒传的或失败的')
          // eslint-disable-next-line no-continue
          continue
        }

        const fileChunkList = this.createFileChunk(filesArr[i])
        // 若不是恢复，再进行hash计算
        if (filesArr[i].status !== 'resume') {
          this.status = Status.hash
          // hash校验，是否为秒传
          // eslint-disable-next-line no-await-in-loop
          filesArr[i].hash = await this.calculateHash(fileChunkList)

          // 若清空或者状态为等待，则跳出循环
          if (this.status === Status.wait) {
            console.log('若清空或者状态为等待，则跳出循环')
            break
          }

          console.log('handleUpload -> hash', filesArr[i].hash)
        }

        this.status = Status.uploading
        // eslint-disable-next-line no-await-in-loop
        const verifyRes = await this.verifyUpload(filesArr[i].name, filesArr[i].hash)
        if (verifyRes.data) {
          filesArr[i].status = fileStatus.secondPass
          filesArr[i].uploadProgress = 100
          this.isAllStatus()
        } else {
          console.log('开始上传文件----》', filesArr[i].name)
          filesArr[i].status = fileStatus.uploading

          const getChunkStorage = this.getChunkStorage(filesArr[i].hash)
          filesArr[i].fileHash = filesArr[i].hash // 文件的hash，合并时使用
          filesArr[i].chunkList = fileChunkList.map(({ file }, index) => ({
            fileHash: filesArr[i].hash,
            fileName: filesArr[i].name,
            index,
            hash: `${filesArr[i].hash}-${index}`,
            chunk: file,
            size: file.size,
            uploaded: getChunkStorage && getChunkStorage.includes(index), // 标识：是否已完成上传
            progress: getChunkStorage && getChunkStorage.includes(index) ? 100 : 0,
            status: getChunkStorage && getChunkStorage.includes(index) ? 'success' : 'wait', // 上传状态，用作进度状态显示
          }))

          this.$set(filesArr, i, filesArr[i])

          console.log('handleUpload ->  this.chunkData', filesArr[i])
          // eslint-disable-next-line no-await-in-loop
          await this.uploadChunks(filesArr[i])
        }
      }
    },
    // 将切片传输给服务端
    async uploadChunks(data) {
      console.log('uploadChunks -> data', data)
      const chunkData = data.chunkList
      return new Promise(async (resolve, reject) => {
        const requestDataList = chunkData
          .filter(({ uploaded }) => !uploaded)
          .map(({
            fileHash, chunk, fileName, index,
          }) => {
            const formData = new FormData()
            formData.append('md5', fileHash)
            formData.append('file', chunk)
            formData.append('fileName', index) // 文件名使用切片的下标

            return {
              formData,
              index,
              fileName,
            }
          })

        console.log('uploadChunks -> requestDataList', requestDataList)

        try {
          const ret = await this.sendRequest(requestDataList, chunkData)
          console.log('uploadChunks -> chunkData', chunkData)
          console.log('ret', ret)
        } catch (error) {
          // 上传有被reject的
          this.$message.error(`亲 上传失败了,考虑重试下呦${error}`)
          return
        }

        // 合并切片
        const isUpload = chunkData.some(item => item.uploaded === false)
        console.log('created -> isUpload', isUpload)
        if (isUpload) {
          // eslint-disable-next-line no-alert
          alert('存在失败的切片')
        } else {
          // 执行合并
          try {
            await this.mergeRequest(data)
            resolve()
          } catch (error) {
            reject()
          }
        }
      })
    },
    // 并发处理
    sendRequest(forms, chunkData) {
      console.log('sendRequest -> forms', forms)
      console.log('sendRequest -> chunkData', chunkData)
      let finished = 0
      const total = forms.length
      const that = this
      const retryArr = [] // 数组存储每个文件hash请求的重试次数，做累加 比如[1,0,2],就是第0个文件切片报错1次，第2个报错2次

      return new Promise((resolve, reject) => {
        const handler = () => {
          console.log('handler -> forms', forms)
          if (forms.length) {
            // 出栈
            const formInfo = forms.shift()

            const { formData } = formInfo
            const { index } = formInfo

            instance
              .post(`${this.baseUrl}/fileChunk`, formData, {
                onUploadProgress: that.createProgresshandler(chunkData[index]),
                cancelToken: new CancelToken(c => this.cancels.push(c)),
                timeout: 0,
              })
              .then((res) => {
                console.log('handler -> res', res)
                // 更改状态
                // eslint-disable-next-line no-param-reassign
                chunkData[index].uploaded = true
                // eslint-disable-next-line no-param-reassign
                chunkData[index].status = 'success'

                // 存储已上传的切片下标
                this.addChunkStorage(chunkData[index].fileHash, index)

                finished++
                handler()
              })
              .catch((e) => {
                // 若状态为暂停或等待，则禁止重试
                console.log('handler -> this.status', this.status)
                if ([Status.pause, Status.wait].includes(this.status)) return

                console.warn('出现错误', e)
                console.log('handler -> retryArr', retryArr)
                if (typeof retryArr[index] !== 'number') {
                  retryArr[index] = 0
                }

                // 更新状态
                // eslint-disable-next-line no-param-reassign
                chunkData[index].status = 'warning'

                // 累加错误次数
                retryArr[index]++

                // 重试3次
                if (retryArr[index] >= this.chunkRetry) {
                  console.warn(' 重试失败--- > handler -> retryArr', retryArr, chunkData[index].hash)
                  // eslint-disable-next-line prefer-promise-reject-errors
                  return reject('重试失败', retryArr)
                }

                console.log('handler -> retryArr[finished]', `${chunkData[index].hash}--进行第 ${retryArr[index]} '次重试'`)
                console.log(retryArr)

                this.tempThreads++ // 释放当前占用的通道

                // 将失败的重新加入队列
                forms.push(formInfo)
                handler()
              })
          }

          if (finished >= total) {
            resolve('done')
          }
        }

        // 控制并发
        for (let i = 0; i < this.tempThreads; i++) {
          handler()
        }
      })
    },
    // 通知服务端合并切片
    mergeRequest(data) {
      return new Promise((resolve, reject) => {
        const obj = {
          md5: data.fileHash,
          fileName: data.name,
          size: data.size,
          fileChunkNum: data.chunkList.length,
          ...this.uploadArguments,
        }

        instance
          .post(`${this.baseUrl}/fileChunk/merge`, obj, {
            timeout: 0,
          })
          .then((res) => {
            // 清除storage
            if (res.data.code === 2000) {
              // eslint-disable-next-line no-param-reassign
              data.status = fileStatus.success
              console.log('mergeRequest -> data', data)
              clearLocalStorage(data.fileHash)
              // this.$message.success('上传成功');
              // 判断是否所有都成功上传
              this.isAllStatus()
              resolve()
            } else {
              // 文件块数量不对，清除缓存
              clearLocalStorage(data.fileHash)
              // eslint-disable-next-line no-param-reassign
              data.status = fileStatus.error
              this.status = Status.wait
              resolve()
            }
          })
          .catch((err) => {
            console.log('mergeRequest -> err', err)
            // eslint-disable-next-line no-param-reassign
            data.status = fileStatus.error
            reject()
          })
      })
    },
    // 创建文件切片
    createFileChunk(file, size = chunkSize) {
      const fileChunkList = []
      let count = 0
      while (count < file.size) {
        fileChunkList.push({
          file: file.slice(count, count + size),
        })
        count += size
      }
      console.log('createFileChunk -> fileChunkList', fileChunkList)
      return fileChunkList
    },

    // 暂停上传
    handlePause() {
      this.status = Status.pause
      if (this.uploadFiles.length) {
        const currentFile = this.uploadFiles[fileIndex]
        currentFile.status = fileStatus.pause
        // 将当前进度赋值给假进度条
        currentFile.fakeUploadProgress = currentFile.uploadProgress
        console.log('handlePause -> currentFile', currentFile)
      }
      while (this.cancels.length > 0) {
        this.cancels.pop()('取消请求')
      }
    },
    // 恢复上传
    handleResume() {
      this.status = Status.uploading
      console.log('handleResume -> this.uploadFiles[fileIndex]', this.uploadFiles[fileIndex])
      this.uploadFiles[fileIndex].status = fileStatus.resume
      this.handleUpload()
    },
    // 文件上传之前的校验： 校验文件是否已存在
    verifyUpload(fileName, fileHash) {
      return new Promise((resolve) => {
        const obj = {
          md5: fileHash,
          fileName,
          ...this.uploadArguments,
        }
        instance
          .get(`${this.baseUrl}/fileChunk/presence`, { params: obj })
          .then((res) => {
            console.log('verifyUpload -> res', res)
            resolve(res.data)
          })
          .catch((err) => {
            console.log('verifyUpload -> err', err)
          })
      })
    },
    // 生成文件 hash（web-worker）
    calculateHash(fileChunkList) {
      console.log('calculateHash -> fileChunkList', fileChunkList)
      return new Promise((resolve) => {
        this.worker = new Worker('./hash.js')
        this.worker.postMessage({ fileChunkList })
        this.worker.onmessage = (e) => {
          const { percentage, hash } = e.data
          if (this.uploadFiles[fileIndex]) {
            this.uploadFiles[fileIndex].hashProgress = Number(percentage.toFixed(0))
            this.$set(this.uploadFiles, fileIndex, this.uploadFiles[fileIndex])
          }

          if (hash) {
            resolve(hash)
          }
        }
      })
    },
    // 切片上传进度
    createProgresshandler(item) {
      console.log('createProgresshandler -> item', item)
      return (p) => {
        // eslint-disable-next-line radix,no-param-reassign
        item.progress = parseInt(String((p.loaded / p.total) * 100))
        this.fileProgress()
      }
    },
    // 文件总进度
    fileProgress() {
      const currentFile = this.uploadFiles[fileIndex]
      if (currentFile) {
        const uploadProgress = currentFile.chunkList.map(item => item.size * item.progress)
          .reduce((acc, cur) => acc + cur)
        // eslint-disable-next-line radix
        const currentFileProgress = parseInt((uploadProgress / currentFile.size).toFixed(2))

        // 真假进度条处理--处理进度条后移
        if (!currentFile.fakeUploadProgress) {
          currentFile.uploadProgress = currentFileProgress
          this.$set(this.uploadFiles, fileIndex, currentFile)
        } else if (currentFileProgress > currentFile.fakeUploadProgress) {
          currentFile.uploadProgress = currentFileProgress
          this.$set(this.uploadFiles, fileIndex, currentFile)
        }
      }
    },
    // 存储已上传完成的切片下标
    addChunkStorage(name, index) {
      const data = [index]
      console.log('addChunkStorage -> name, data', name, data)
      const arr = getObjArr(name)
      if (arr) {
        saveObjArr(name, [...arr, ...data])
      } else {
        saveObjArr(name, data)
      }
    },
    // 获取已上传完成的切片下标
    getChunkStorage(name) {
      return getObjArr(name)
    },
    // axios 设置axios参数
    setAxios() {
      if (!this.headers) return
      for (const i in this.headers) {
        instance.defaults.headers.common[i] = this.headers[i]
      }

      // 是否携带cookie
      if (this.withCredentials) {
        instance.defaults.withCredentials = true
      }

      // 设置baseUrl
      if (this.baseUrl) {
        instance.defaults.baseURL = this.baseUrl
      }

      // 设置切片大小
      // eslint-disable-next-line prefer-destructuring
      chunkSize = this.chunkSize
    },
    // 清空文件
    clearFiles() {
      console.log('清空文件')
      fileIndex = 0
      this.handlePause()

      // eslint-disable-next-line no-unused-expressions
      this.worker && this.worker.terminate() // 中断worker
      this.status = Status.wait

      Object.assign(this.$data, this.$options.data()) // 重置data所有数据
    },
    // 判断是否都已完成上传
    isAllStatus() {
      const isAllSuccess = this.uploadFiles.every(item => ['success', 'secondPass', 'error'].includes(item.status))
      console.log('mergeRequest -> isAllSuccess', isAllSuccess)
      if (isAllSuccess) {
        this.status = Status.done
        this.$emit('success')
      }
    },
  },
}
</script>
<style scoped lang="scss">
    .simple-upload-container {
        border: 1px solid #d2d2d2;
        border-radius: 4px;
        background-color: #fff;
        padding-bottom: 15px;
        padding: 10px;

        .progress-box {
            width: 100%;
        }

        .total-progress {
            margin-bottom: 15px;

            .btns {
                position: relative;

                .select-file-input {
                    position: absolute;
                    display: inline-block;
                    left: 0;
                    top: 0;
                    border: none;
                    opacity: 0;
                    width: 96px;
                    height: 28px;
                }
            }
        }

        .file-list {
            .list-item {
                padding: 8px 10px;
                display: flex;
                justify-content: center;
                justify-items: center;
                line-height: 25px;
                position: relative;

                &:hover .item-chunk-box {
                    display: block;
                }

                div {
                    flex: 1;
                    margin-top: 6px;
                }

                .item-name {
                    overflow: hidden;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                    margin-right: 6px;

                    .svg-icon {
                        font-size: 22px;
                        vertical-align: sub;
                    }
                }

                .item-status {
                    flex: 0 0 10%;
                    text-align: center;
                    text-align: left;

                    .el-icon-circle-check {
                        color: #67c23a;
                    }

                    .el-icon-circle-close {
                        color: #f00;
                    }
                }

                .item-chunk-box {
                    display: none;
                    transition: all 3s;
                    position: absolute;
                    top: 0;
                    left: 40px;
                    z-index: 10;
                }

                .item-progress {
                    flex: 0 0 60%;
                }
            }
        }

        .upload-tip {
            font-size: 12px;
            color: #606266;
            margin-top: 7px;
        }

        .el-progress {
            width: 80%;
            display: inline-block;
        }

        > > > .el-collapse-item__header {
            height: auto;
        }
    }
</style>
